import random
import math
from collections import deque
from pprint import pprint
from math import pi
com_municate = 0 # just some globals for communication, these ALWAYS start with com_
com_goto_flag = [0,0,0]
MAX_goto_flag = []#[4,2,2]
#self.goalList = [(500,400),(220,220),(220,580)]
com_team = ''
com_ammo_nav = {} # ammo points which will be taken by teammate
com_enemy_nav = {} # enemies which will be shot by teammate
directions = []
MAX_SPEED = 40
MAX_SHOT_DISTANCE  = 60
round = 0 # number of rounds
first = 0
com_agents = [[0,(-1,-1), 0]]*8 # (ammo, controlpointCoords, camping (0 = on way, 1 = camping, 2 = leave spot))
#com_agents[observation['id'] - 1]

class AgentBrain():
    def __init__(self):
        """Put any initialization code here."""
        #Create navigational instance
        self.nav = Navigate()
        self.team = ''
        self.prevP = Point(-1,-1,0,False)

    def action(self, observation):
        #pprint(observation)
        global round, first, MAX_goto_flag, directions, com_agents, com_team

        self.team = observation['team']
        self.nav.id = observation['id']

        com_agents[observation['id'] - 1][0] = observation['ammo'] # set the ammo so teammates know about it

        #A lot of observation dependent things that need to happen once
        if first == 0:
            com_team = observation['team']
            first = observation['id']
            if observation['team'] == 'Red':
                MAX_goto_flag = [4,2,3]
            elif observation['team'] == 'Blue':
                MAX_goto_flag = [4,3,2]
            else:
                print "LOLWUT: We are color",observation['team']

        if observation['id'] == first:
            #print '\t' #First active agent this round' #reset some variables here?!
            com_ammo_nav = {}
            com_enemy_nav = {}
            directions = []
            round = round + 1

        loc = 'location'
        (x,y) = observation[loc]

        #Create point for current location
        cp = Point(x,y,0,False)

        #Reset path if just killed

        if observation['respawn']:
            #print '%i omfg we got killed at %s' % (round, str(self.prevP.coord))
            self.nav.setPath(observation[loc][0],observation[loc][1])
        elif cp.getDist(self.prevP) > MAX_SPEED + 4:
            #print '%i --Moved a lot from %s : %d, resetting path..(killed but not properly sent by server)'
            #% (round, str(self.prevP.coord), cp.getDist(self.prevP))
            self.nav.setPath(observation[loc][0],observation[loc][1])

        id = observation['id']
        self.controlPoints(cp,id)

        if com_agents[id-1][2] == 2:

            self.nav.setPath(x,y)

        #Store in action format

        self.prevP = cp #

        #check for enemies to 'take'
        (sh,t) = self.nav.shoot(cp, self.team, observation)
        s = 0 # for now: don't move if you shoot something

        if sh == 0 and t == 0: # we are not shooting (and not preparing to shoot)
            #check for ammo to take + add to path if going for
            self.nav.getAmmo(cp, observation)

            #Get speed and turn based on current path
            (s,t) = self.nav.getMove(observation[loc][0], observation[loc][1], \
                observation['direction'])
        elif sh == 1:
            com_agents[observation['id'] - 1][0] = observation['ammo'] - 1 # more accurate update of ammo

        """#WORK IN PROGRESS#
        #Absolute angle
        a = t + observation['direction']
        #Normal angle
        a = math.pi - a

        #Start and end points of trajectory line
        sl = (x + math.cos(a-math.pi)*(3), y + math.sin(a-math.pi)*(3))
        el = (x + math.cos(a)*(s+3), y + math.sin(a)*(s+3))

        #Monster equation that makes 2 lines describing the agents path
        lines = [(((sl[0] + math.cos(a-0.5*math.pi)*(3),sl[1] + \
        math.sin(a-0.5*math.pi)*(3)), (el[0] + math.cos(a-0.5*math.pi)*(3), \
        el[1] + math.sin(a-0.5*math.pi)*(3)))), ((sl[0] + \
        math.cos(a+0.5*math.pi)*(3),sl[1] + math.sin(a+0.5*math.pi)*(3)), \
        (el[0] + math.cos(a+0.5*math.pi)*(3),el[1] + math.sin(a+0.5*math.pi)* \
        (3)))]

        fl = False
        for d in directions:
            if self.nav.intersect(d[0],d[1],lines[0][0],lines[0][1]) \
                or self.nav.intersect(d[0],d[1],lines[1][0],lines[1][1]):
                fl = True
                break
        if fl:
            s = 0
        else:
            directions.extend(lines)
        """#END WORK IN PROGRESS#

        action = {
            'turn': t,
            'speed': s,
            'shoot': sh
        }
        return action

    def controlPoints(self,cPoint,id):
        global com_agents

        cVal = com_agents[id-1]
        if cVal[2] != 1:
            gPoint = Point(cVal[1][0],cVal[1][1],0,False)
            if cPoint.getDist(gPoint) < MAX_SPEED:
                camp = []
                for i in com_agents:
                    if i[1] == cVal[1] and i[2] == 1:
                        camp = i
                        break
                if camp == []:
                    cVal[2] = 1
                elif cVal[0] > camp[0]:
                    cVal[2] = 1
                    camp[2] = 2
                else:
                    cVal[2] = 2


class Navigate:
    def __init__(self):
        #List regions > List points > Tuple x,y,cross
        self.initList =  [ [ (625,100,1)] , \
            [ (625,700,2) ], \
            [ (275,750,3),(50,525,4) ], \
            [ (50,475,4),(50,325,5),(275,400,7) ], \
            [ (50,275,5),(275,50,6) ], \
            [ (575,100,1),(575,700,2),(325,750,3),(325,400,7),(325,50,6) ] ]
        #Control-points: first one is currently preferred, then 2nd if blue
        self.goalList = [(500,400),(220,220),(220,580)]
        #Using initList, creation connectivity graph
        self.constructGraph()
        #Current list of waypoints in path
        self.path = deque([])
        #Current history (length == 2) of previous points
        self.hist = deque([])
        self.goalInt = -1
        self.id = 0

    def shoot(self,cp,team,observation):
        if observation['ammo'] == 0:
            return (0,0) #grr we don't got ammo to shoot :/
        global round
        for agent in observation['agents']:
            if agent['team'] != team:
                if str(agent['location']) in com_enemy_nav:
                    print '%i enemy %s getting shot!' % (round, str(agent['location']))
                else:
                    ep = Point(agent['location'][0],agent['location'][1],0,False)
                    # TODO: add line interference check instead of region for shooting
                    if ep.region == cp.region:
                        #Get distance and angle
                        d = cp.getDist(ep)
                        a = cp.getAngle(ep)
                        #print '\t dist: %s ang: %s' % (d, a)
                        movesReq = 0
                        #Recalc angle based on current direction
                        a -= observation['direction']
                        if abs(a) > math.pi:
                            if a >= 0:
                                a -= 2*math.pi
                            else:
                                a += 2*math.pi

                        #print 'recalc angle: '+str(a)
                        #If angle is greater than maximum turn, means you can't reach this turn
                        #movesReq += int(math.ceil(abs(a) / (math.pi / 3))) - 1
                        movesReq += int(math.ceil(d / MAX_SHOT_DISTANCE)) - 1
                        if movesReq < 1:
                            print '%i Shoot enemy at %s' % (round, str(agent['location']))
                            #(s,t) = (d,a) # use this dist/angle instead of current

                            #let other agents known this enemy is ours!
                            com_enemy_nav[str(agent['location'])] = observation['id']
                            sh = 0
                            if abs(a) <= math.pi / 3:
                                sh = 1
                            return (sh, a) # no need to check other enemies
              #          else:
              #              print 'Shoot NOT: too far'
              #      else:
              #          print 'Shoot NOT: tother region, not going!'


            #print "av_agent: %i , location: %s , direct: %f  ,colour: %s " % (av_agent['id'],
            #av_agent['location'], av_agent['direction'], av_agent['team'])
            # kinda same comments as with the ammo
        return (0, 0) #aw, nothing to shoot :/

    def getAmmo(self, cp, observation):
        global com_ammo_nav

        for ammo_av in observation['ammopacks']:
            #print "ammo_av location: %i %i " % (ammo_av['location'][0], ammo_av['location'][1])
            #when to take ammo?
            #-rank available ammo according to easyest to take? (probably overkill)
            #-always take if without ammo?
            #-always take if easy to take? (ie getting not much further from destination)
            #-never take if it requires more then 2 movements in 'wrong' direction?
            if str(ammo_av['location']) in com_ammo_nav:
                pass #print '\tGRR ammo getting picked by teammate'
            else:
                #create point for this ammo pack
                #ammo_reg = self.nav.getRegion(ammo_av['location'][0],ammo_av['location'][1])
                #print 'ammo region: '+str(ammo_reg)
                ap = Point(ammo_av['location'][0],ammo_av['location'][1],0,False)

                if ap.region == cp.region:
                    #Get distance and angle
                    d = cp.getDist(ap)
                    a = cp.getAngle(ap)
                    #print '\t dist: %s ang: %s' % (d, a)
                    movesReq = 0
                    #Recalc angle based on current direction
                    a -= observation['direction']
                    if abs(a) > math.pi:
                        if a >= 0:
                            a -= 2*math.pi
                        else:
                            a += 2*math.pi

                    #print 'recalc angle: '+str(a)
                    #Calculate required moves
                    movesReq += (int(math.ceil(abs(a) / (math.pi / 3))) - 1)
                    #If not near goal, do not rotation gets more weight
                    if (len(self.path) > 1):
                        movesReq *= 5
                    movesReq += int(math.ceil(d / MAX_SPEED)) - 1
                    if movesReq <= math.floor(9/(observation['ammo'] + 1)):
                           # print 'getting ammo!'
                            #(s,t) = (d,a) # use this dist/angle instead of current
                            self.path.appendleft(ap)
                            #let other agents known this ammo is ours!
                            com_ammo_nav[str(ammo_av['location'])] = observation['id']
                            return # no need to check other ammo points
               #     else:
               #         print 'ammo too far'
               # else:
               #     print 'other region, not going!'

    def constructGraph(self):
        #Create region list
        self.regions = []
        #For each region
        for r in self.initList:
            reg = []
            #For each points
            for p in r:
                reg.append(Point(p[0],p[1],p[2],True))
            #Connect all points in the same region
            for p1 in reg:
                for p2 in reg:
                    p1.addNeigh(p2)
            #Add region to region list
            self.regions.append(reg)
        #Connect all points in the same crossing
        #C = Cross counter, starting at crossID 1
        c = 1
        while True:
            #Store all the points of that crossID C
            t = []
            #For each region
            for r in self.regions:
                for p in r:
                    if p.cross == c:
                        t.append(p)
            #If there are no points of crossID C
            if len(t) == 0:
                break
            #Connect all points in the same crossing
            for p1 in t:
                for p2 in t:
                    p1.addNeigh(p2)
            #Check the next crossing
            c += 1

    def setPath(self,x,y):
        global com_agents
        #Reset history
        self.hist = deque([])
        com_agents[self.id - 1][2] = 0
        #Create a not really random goal from control-point list
        #g = self.goalList[random.randint(0,2)]

        global com_goto_flag

        goalOld = -1
        if self.goalInt > -1: # decrease previous goto goal
            goalOld = com_goto_flag[self.goalInt]
            com_goto_flag[self.goalInt] = 0

        #random 'distribution' of the goals
        distr_flag = [MAX_goto_flag[0] - com_goto_flag[0], \
                        MAX_goto_flag[1] - com_goto_flag[1], \
                        MAX_goto_flag[2] - com_goto_flag[2]]
        # how many places where?
        rdist = random.randint(0,sum(distr_flag) - 1)
        r = 0
        if rdist < distr_flag[0]:
            r = 0
        elif rdist < distr_flag[0] + distr_flag[1]:
            r = 1
        else:
            r = 2
        g = self.goalList[r]
        com_agents[self.id - 1][1] = g # set the controlpointCoords

        if goalOld > -1:
            com_goto_flag[self.goalInt] = goalOld -1

        com_goto_flag[r] = com_goto_flag[r] + 1
        self.goalInt = r
        #com_goto_flag = [0,0,0]
        #MAX_goto_flag = [5,3,3]

        #gr = self.getRegion(g[0],g[1])
        gp = Point(g[0],g[1],0,False)

        #Creat point for current loc
        #cr = self.getRegion(x,y)
        cp = Point(x,y,0,False)
        #Connect current point with others in region
        for p in self.regions[cp.region]:
            cp.addNeigh(p)
        #Queue of breadth-first paths and their total distance
        #Current path distance is 0 and consists only of current point
        q = deque([[0,[cp]]])
        #Minimal distance and path to goal
        minDist = -1
        minPath = []
        #While still elements in queue
        while len(q) > 0:
            #Pop path from queue
            p = q.popleft()
            #If the last waypoint in the path is in the goal region
            if p[1][-1].region == gp.region:
                #Distance from last waypoint to goal
                d = p[1][-1].getDist(gp)
                #If total distance is minimal
                if minDist == -1 or p[0] + d < minDist:
                    #Add the goal the path
                    np = p[1][:]
                    np.append(gp)
                    #Store path as current minimum
                    minDist = p[0] + d
                    minPath = np
            #Else: Keep expanding paths
            else:
                #For each point that is connected to the last waypoint
                #in the path
                for i in p[1][-1].neigh:
                    #If the new point is not yet in the path
                    if i[0] not in p:
                        #Add new point to path
                        np = p[1][:]
                        np.append(i[0])
                        #Increase the total distance
                        nd = p[0]+i[1]
                        #If current distance is smaller than minimum
                        if minDist == -1 or nd < minDist:
                            #Append the path to the end of the queue
                            q.append([nd,np])
        self.path = deque(minPath)


    def getMove(self,x,y,dir):
        #Create point for current location
        cp = Point(x,y,0,False)


        #Determine total movement w.r.t. history
        dt = 0
        #For each point in the history
        for i in self.hist:
            #Increase the total distance
            dt += cp.getDist(i)
        #Add current point to history
        self.hist.append(cp)
        #If length of history > 2
        if len(self.hist) > 2:
            #Pop 1 element from history
            self.hist.popleft()


        if dt < 1 and len(self.hist) == 2: #If total movement < 1: Stuck, make random turn
            return (30,random.random()*pi/1.5 - pi/3)
        else:
            #Init distance and angle
            d = 0
            a = dir
            while len(self.path) > 0:
                #If there are no more waypoints left
                #if len(self.path) == 0:
                    #Get new path
                #    self.setPath(x,y)
                #Pop waypoint from the current path
                wp = self.path.popleft()
                #Get distance and angle
                d = cp.getDist(wp)
                a = cp.getAngle(wp)
                #If remaining distance > 4; meaning you have not reached
                #the waypoint yet
                if d > 5 or len(self.path) == 0:
                    #Reappend waypoint to front of path
                    self.path.appendleft(wp)
                    break

            # we are at at out goalpoint and we were planning to camp; let's camp!
            if com_agents[self.id - 1][2] == 1 and d < 2: #TODO set back to == 1
                #set correct angle for this spot
                d = 0
                a = self.getPrefDirection(com_agents[self.id - 1][1])
      #      else:
            #Recalc angle based on current direction
            a -= dir
            if abs(a) > math.pi:
                if a >= 0:
                    a -= 2*math.pi
                else:
                    a += 2*math.pi
            #If angle is greater than maximum turn, do not move forward
            if abs(a) > math.pi / 3:
                d = 0

            #Return distance,angle
            return (d,a)
    def getPrefDirection(self, xy):
        (x,y) = xy
        #print 'getPrefDirection %i and %i' % (x, y)
        if com_team == 'Red':
            if x == 220 and y == 220:
                return 0*pi

            return 0.5*pi
        else:
            if x == 220 and y == 220:
                return 0*pi
            return 1.5*pi

    def intersect(self,a1,a2,b1,b2):
        p3 = ((a1[0] - a2[0])*(b1[1] - b2[1]) - (a1[1] - a2[1])*(b1[0] - b2[0]))
        if (p3 != 0):
            p1 = (a1[0]*a2[1] - a1[1]*a2[0])
            p2 = (b1[0]*b2[1] - b1[1]*b2[0])
            x = (p1*(b1[0] - b2[0]) - (a1[0] - a2[0])*p2) / p3
            if min(a1[0],a2[0]) <= x and x <= max(a1[0],a2[0]):
                y = (p1*(b1[1] - b2[1]) - (a1[1] - a2[1])*p2) / p3
                return True
        return False

class Point:
    def __init__(self,x,y,cross,way):
        #Random offset for waypoints, but not for control-points
        if way:
            x += random.randint(-20,20)
            y += random.randint(-20,20)
        #Store point variables
        self.coord = (x,y)
        self.cross = cross
        self.region = self.getRegion(x, y)
        self.neigh = []
        self.check = []
        self.waypoint = way

    def addNeigh(self,n):
        #Check if it is not the same point or already connected
        if n != self and n not in self.check:
            #Compute distance between points
            d = self.getDist(n)
            #Store as point,distance tuple
            self.neigh.append((n,d))
            #Store as point
            self.check.append(n)

    def getDist(self,n):
        #Compute the distance between the points
        x = n.coord[0]-self.coord[0]
        y = n.coord[1]-self.coord[1]
        d = math.sqrt(x**2+y**2)
        return d

    def getAngle(self,n):
        #Compute the angle between points
        x = n.coord[0]-self.coord[0]
        y = n.coord[1]-self.coord[1]
        a = -math.atan2(-y,x)
        return a
    #Get the region, pretty hard-coded
    def getRegion(self,x,y):
        if x > 798 or x < 2 or y > 798 or y < 2:
            return -1 # to prevent going to ammo spawnpoints on walls
        if x >= 600:
            if y < 400:
                return 0 # region A
            return 1 # region B
        if x >= 300:
            return 5 # region F
        if y >= 500:
            return 2 # region C
        if y >= 300:
            return 3 # region D
        return 4 # region E
